package com.wang.system.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.pagehelper.PageHelper;
import com.wang.common.core.constants.Constant;
import com.wang.common.core.enums.ResultCode;
import com.wang.common.security.exception.ServiceException;
import com.wang.system.domain.exam.Exam;
import com.wang.system.domain.exam.ExamQuestion;
import com.wang.system.domain.exam.dto.ExamAddDTO;
import com.wang.system.domain.exam.dto.ExamEditDTO;
import com.wang.system.domain.exam.dto.ExamQueryDTO;
import com.wang.system.domain.exam.dto.ExamQuestionDTO;
import com.wang.system.domain.exam.vo.ExamDetailVO;
import com.wang.system.domain.exam.vo.ExamVO;
import com.wang.system.domain.question.Question;
import com.wang.system.domain.question.vo.QuestionVO;
import com.wang.system.manager.ExamCacheManager;
import com.wang.system.mapper.ExamMapper;
import com.wang.system.mapper.ExamQuestionMapper;
import com.wang.system.mapper.QuestionMapper;
import com.wang.system.service.IExamService;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import static com.baomidou.mybatisplus.extension.toolkit.Db.saveBatch;

@Service
public class ExamServiceImpl implements IExamService {

    @Autowired
    private ExamMapper examMapper;

    @Autowired
    private QuestionMapper questionMapper;

    @Autowired
    private ExamQuestionMapper examQuestionMapper;

    @Autowired
    private ExamCacheManager examCacheManager;

    @Override
    public List<ExamVO> list(ExamQueryDTO examQueryDTO) {
        //startPage()开启分页功能, 后续执行的SQL回自动进行分页处理
        PageHelper.startPage(examQueryDTO.getPageNum(),examQueryDTO.getPageSize());
        //根据条件查询出对应的记录
        return examMapper.selectExamList(examQueryDTO);
    }

    @Override
    public String add(ExamAddDTO examAddDTO) {

        //1.校验竞赛信息正确性
        checkExamTitleAndTime(examAddDTO,null);

        Exam exam = new Exam();

        //将examAddDTO中的属性值复制到exam中.
        BeanUtil.copyProperties(examAddDTO,exam);

        //设置默认未开赛
        exam.setStatus(Constant.FALSE);

        //将新增竞赛进行插入
        examMapper.insert(exam);

        //返回竞赛id
        return exam.getExamId().toString();
    }

    @Override
    public boolean questionAdd(ExamQuestionDTO examQuestionDTO) {

        //1.根据竞赛 id 从数据库中查到要添加题目的竞赛.
        Exam exam = getExamById(examQuestionDTO.getExamId());

        //2.检查要添加题目的竞赛是否开赛 (已开赛则不允许添加题目).

        checkExamIsStarted(exam);

        //3.检查要添加题目的竞赛是否已经发布 (已发布不允许添加题目).
        if (Constant.TRUE.equals(exam.getStatus())){
            throw new ServiceException(ResultCode.EXAM_IS_PUBLISHED);
        }

        //4.获取要添加题目的id集合
        //采用集合的原因: 一次添加的题目可以是多个 且 题目不能重复.
        Set<Long> questionIdSet = examQuestionDTO.getQuestionIdSet();

        //如果集合为空,说明没有添加题目,直接返回
        if (CollectionUtil.isEmpty(questionIdSet)){
            return true;
        }

        //5.从question表中批量查询所有的要添加的题目
        //SELECT * FROM question WHERE question_id IN (?, ?, ?, ...); -- 问号数量取决于questionIdSet的大小
        List<Question> questionList = questionMapper.selectBatchIds(questionIdSet);

        //6.如果查询结果为空或者查询出的题目数量比要添加的题目数量少 --> 说明有的要添加的题目根本就不存在 --> 抛出异常
        if (questionList == null || questionList.isEmpty() || questionList.size() < questionIdSet.size()){
            throw new ServiceException(ResultCode.EXAM_QUESTION_NOT_EXISTS);
        }

        //为关联表中保存竞赛的题目
        return saveExamQuestion(exam,questionIdSet);
    }

    @Override
    public int questionDelete(Long examId, Long questionId) {
        //1.根据竞赛 id 从数据库中查到要删除题目的竞赛.
        Exam exam = getExamById(examId);

        //2.检查要删除题目的竞赛是否开赛 (已开赛则不允许删除题目).
        checkExamIsStarted(exam);

        //3.检查要删除题目的竞赛是否已经发布 (已发布不允许删除题目).
        if (Constant.TRUE.equals(exam.getStatus())){
            throw new ServiceException(ResultCode.EXAM_IS_PUBLISHED);
        }

        //4.从数据库中删除该竞赛中的指定题目
        //DELETE FROM exam_question WHERE exam_id = #{examId} AND question_id = #{questionId};
        return examQuestionMapper.delete(new LambdaQueryWrapper<ExamQuestion>()
                .eq(ExamQuestion::getExamId,examId)
                .eq(ExamQuestion::getQuestionId,questionId));
    }

    @Override
    public ExamDetailVO detail(Long examId) {

        //1.设置竞赛基本信息
        //首先根据传入id查询到相应的竞赛信息exam
        Exam exam = getExamById(examId);

        //创建一个ExamDetailVO对象
        ExamDetailVO examDetailVO = new ExamDetailVO();

        //将exam中的属性值复制到examDetailVO中
        BeanUtil.copyProperties(exam,examDetailVO);

        //2.设置竞赛题目信息
        //根据竞赛id查询竞赛对应的题目列表
        List<QuestionVO> questionVOList = examQuestionMapper.selectExamQuestionList(examId);
        if (CollectionUtil.isEmpty(questionVOList)){
            //如果题目列表为空,则直接返回
            return examDetailVO;
        }
        //如果不为空,设置题目列表
        examDetailVO.setExamQuestionList(questionVOList);

        //返回竞赛详情
        return examDetailVO;
    }

    @Override
    public int edit(ExamEditDTO examEditDTO) {

        //1.先根据id从数据库中查到到要编辑的竞赛
        Exam exam = getExamById(examEditDTO.getExamId());

        //2.检查当前竞赛是否已经开赛 (已经开赛的竞赛不允许编辑)
        checkExamIsStarted(exam);

        //3.检查当前竞赛是否已经发布 (已发布的竞赛不允许编辑)
        if (Constant.TRUE.equals(exam.getStatus())){
            throw new ServiceException(ResultCode.EXAM_IS_PUBLISHED);
        }

        //4.检查编辑后的竞赛的标题, 开始时间, 结束时间.

        //注意要把本身排除掉,所以要传入id
        //由于ExamAddDTO是ExamEditDTO的父类,所以可以传入ExamEditDTO
        checkExamTitleAndTime(examEditDTO, examEditDTO.getExamId());


        //如果竞赛标题和开始结束时间都合法, 则进行对竞赛基本信息更新操作
        exam.setStartTime(examEditDTO.getStartTime());
        exam.setEndTime(examEditDTO.getEndTime());
        exam.setTitle(examEditDTO.getTitle());

        //更新数据库
        return examMapper.updateById(exam);
    }

    /**
     * 获取竞赛并校验正确性
     * @param examId 竞赛id
     * @return 返回获取到的竞赛
     */
    private Exam getExamById(Long examId) {
        //从数据库中查到传入id对应的exam
        Exam exam = examMapper.selectById(examId);
        //首先检查竞赛是否存在
        if (exam == null){
            throw new ServiceException(ResultCode.FAILED_RESOURCE_NOT_EXISTS);
        }
        return exam;
    }

    @Override
    public int delete(Long examId) {

        //1.根据竞赛 id 从数据库中查到要删除的竞赛
        Exam exam = getExamById(examId);


        //2.检查要删除的竞赛是否开赛 (已开赛则不允许删除).
        checkExamIsStarted(exam);

        //3.检查要删除的竞赛是否已经发布 (已发布不允许删除).
        if (Constant.TRUE.equals(exam.getStatus())){
            throw new ServiceException(ResultCode.EXAM_IS_PUBLISHED);
        }

        //4.删除库中当前竞赛中的所有题目信息
        //DELETE FROM exam_question WHERE exam_id = #{examId};  -- 删除所有关联到指定examId的题目记录
        examQuestionMapper.delete(new LambdaQueryWrapper<ExamQuestion>().
                eq(ExamQuestion::getExamId,examId));

        //5.删除库中当前竞赛的基本信息
        return examMapper.deleteById(examId);
    }


    @Override
    public int publish(Long examId) {
        //1.根据竞赛 id 从数据库中查到要发布的竞赛
        Exam exam = getExamById(examId);

        //2.判断竞赛是否结束 (已经结束的不能发布)
        if (exam.getEndTime().isBefore(LocalDateTime.now())){
            throw new ServiceException(ResultCode.EXAM_IS_FINISHED);
        }

        //3.判断竞赛种是否有题目 (没有题目的竞赛不可以发布)
        // SELECT COUNT(*) FROM exam_question WHERE exam_id = #{examId};  -- 统计指定竞赛种题目的数量.
        Long questionCount = examQuestionMapper.selectCount(
                new LambdaQueryWrapper<ExamQuestion>()
                        .eq(ExamQuestion::getExamId, examId)
        );

        if (questionCount == null || questionCount <= 0){
            throw new ServiceException(ResultCode.EXAM_HAS_NO_QUESTION);
        }

        //4.修改竞赛的状态参数为true,表示已发布.
        exam.setStatus(Constant.TRUE);

        //---先更新数据库
        int result = examMapper.updateById(exam);

        //5.竞赛发布之后,把竞赛数据存储到redis中,以便c端可以访问

        //---再把数据存入缓存
        examCacheManager.addCache(exam);

        return result;
    }

    @Override
    public int cancelPublish(Long examId) {

        //1.根据竞赛 id 从数据库中查到要撤销发布的竞赛
        Exam exam = getExamById(examId);

        //2.判断竞赛是否已经开赛 (已经开赛的不可以撤销发布) 注意这里不需要校验竞赛是否发布,因为接口本身就是撤销发布
        checkExamIsStarted(exam);

        //3.判断竞赛是否结束 (已经结束的不可以撤销发布)
        if (exam.getEndTime().isBefore(LocalDateTime.now())){
            throw new ServiceException(ResultCode.EXAM_IS_FINISHED);
        }

        //4.修改竞赛的状态参数为 FALSE, 表示撤销发布
        exam.setStatus(Constant.FALSE);

        //---先更新数据库
        int result = examMapper.updateById(exam);

        //---再从redis中删除数据
        examCacheManager.deleteCache(examId);

        //返回结果
        return result;
    }

    /**
     * 检查当前竞赛是否已经开赛
     * @param exam 传入的竞赛
     */
    private void checkExamIsStarted(Exam exam) {
        LocalDateTime startTime = exam.getStartTime();
        if (startTime.isBefore(LocalDateTime.now())){
            //如果竞赛开始时间早于当前时间,说明竞赛已经开始
            throw new ServiceException(ResultCode.EXAM_IS_STARTED);
        }
    }

    /**
     * 检查竞赛的参数是否合法
     * @param examAddDTO
     */
    private void checkExamTitleAndTime(ExamAddDTO examAddDTO, Long examId) {

        //1.校验竞赛标题

        //竞赛标题不可重复

        //如果是竞赛编辑: 我们可以不改变竞赛标题,此时如果查询是否有标题重复的竞赛时,就会把库中的编辑前数据查到,从而判定为标题重复,无法更新.
        //但是实际上是能进行更新的(我们完全可以不修改竞赛标题)
        //所以我们就需要提前根据id将要编辑的题目排除在查询范围之外 (也就是说编辑后的竞赛和编辑前的竞赛标题是可以相同的).
        //这样一来,判断标题重复时就会把我们要编辑的这道题目排除在外

        //SELECT * FROM exam WHERE title = #{examAddDTO.title} AND exam_id != #{examId}
        //( 如果 examId 不为 null, 才添加条件 exam_id!= #{examId} )
        List<Exam> exams = examMapper.selectList(
                new LambdaQueryWrapper<Exam>()
                        .eq(Exam::getTitle, examAddDTO.getTitle())
                        .ne(examId != null, Exam::getExamId,examId)); //排除掉自身,即id相等的情况

        //如果不为空,则说明该标题的竞赛已经存在
        if (CollectionUtil.isNotEmpty(exams)){
            throw new ServiceException(ResultCode.FAILED_RESOURCE_ALREADY_EXISTS);
        }

        //2.校验竞赛时间
        //竞赛开始时间必须晚于当前时间
        if (examAddDTO.getStartTime().isBefore(LocalDateTime.now())){
            throw new ServiceException(ResultCode.EXAM_START_TIME_BEFORE_CURRENT_TIME);
        }

        //竞赛开始时间必须早于竞赛结束时间
        if (examAddDTO.getStartTime().isAfter(examAddDTO.getEndTime())){
            throw new ServiceException(ResultCode.EXAM_START_TIME_AFTER_END_TIME);
        }
    }

    /**
     * 为指定的竞赛批量保存竞赛题目
     * @param exam 指定的竞赛
     * @param questionIdSet 要添加的题目
     * @return 返回是否保存成功
     */
    private boolean saveExamQuestion(Exam exam,Set<Long> questionIdSet){

        int num = 1; //题目的顺序
        Long examId = exam.getExamId(); //竞赛id
        List<ExamQuestion> examQuestions = new ArrayList<>();

        //遍历questionIdSet,将其封装为ExamQuestion对象,添加顺序,并添加到列表examQuestions中.
        for (Long questionId : questionIdSet){
            ExamQuestion examQuestion = new ExamQuestion();
            examQuestion.setExamId(examId);
            examQuestion.setQuestionId(questionId);
            examQuestion.setQuestionOrder(num++);
            examQuestions.add(examQuestion);
        }

        //使用MyBatis-Plus中service层的批量添加的接口方法 (saveBatch方法),减少网络连接的性能开销
        return saveBatch(examQuestions);
    }
}